<?php
// +----------------------------------------------------------------------
// | KITEGO-Admin「开箱即用」「人人全栈」
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2024 https://www.kitego.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed KITEGO并不是自由软件，未经许可不能去掉KITEGO相关版权
// +----------------------------------------------------------------------
// | Author: KITEGO Team <bd@kitego.cn>
// +----------------------------------------------------------------------

namespace kitego\extend\login\admin;

use app\dao\setting\AdminTokenLogDao;
use app\dao\setting\SystemAdminDao;
use kitego\enum\ApiCodeEnum;
use kitego\extend\login\admin\enum\AdminLogoutMessageEnum;
use kitego\services\common\CacheService;
use think\facade\Db;

class AdminLoginBase implements AdminLoginInterface
{
    private $jwtKey; // JWT密钥

    private $accessTtl; // Access Token有效期（秒）

    private $refreshTtl; // Refresh Token有效期（秒）

    private $refreshRedisPrefix = 'admin_jwt_refresh:'; // 刷新token Redis前缀

    private $logoutListRedisPrefix = 'admin_jwt_logout_list:'; // 注销列表

    public function __construct()
    {
        $this->jwtKey = config('adminjwt.secret_key');
        $this->accessTtl = config('adminjwt.access_ttl');
        $this->refreshTtl = config('adminjwt.refresh_ttl');
    }

    public function login($params)
    {
    }

    /**
     * 登录生成Token
     */
    public function generateJwtToken($systemAdmin)
    {
        // 角色权限 // 超管、管理员、员工
        $adminRole = ['super', 'admin', 'staff'];

        $deviceId = $this->getDeviceId(); // 设备指纹

        // 生成Tokens
        $tokens = $this->generateTokens($adminRole, $systemAdmin, $deviceId);

        return success([
            'adminToken' => $tokens,
            'adminInfo' => [
                'adminRole' => $adminRole,
                'userId' => $systemAdmin['id'],
                'nickname' => $systemAdmin['nickname'],
                'avatar' => $systemAdmin['avatar'],
            ]
        ]);
    }

    /**
     * 授权临期刷新Token
     */
    public function refreshToken($refreshToken)
    {
        if (empty($refreshToken)) abort(ApiCodeEnum::UNAUTHORIZED, '刷新Token不能为空');

        $refreshTokenDecoded = getAdminJWT($refreshToken);

        $storedRefreshToken = CacheService::get($this->refreshRedisPrefix . $refreshTokenDecoded->jti);
        if (!$storedRefreshToken || $storedRefreshToken['used']) abort(ApiCodeEnum::UNAUTHORIZED, '刷新Token无效或已使用');

        $deviceId = $this->getDeviceId();
        if ($storedRefreshToken['deviceId'] !== $deviceId) abort(ApiCodeEnum::UNAUTHORIZED, '设备不匹配');

        // 标记旧刷新Token为已使用并保留短时间用于防止重放攻击
        $this->updateRefreshTokenRedis(
            $storedRefreshToken['userId'], 
            $storedRefreshToken['deviceId'], 
            $refreshTokenDecoded->jti, 
            $this->accessTtl, 
            true
        );

        // 更新已注销列表
        $this->updateLogoutListRedis(
            $refreshTokenDecoded->access_jti, 
            $refreshTokenDecoded->access_exp, 
            $refreshTokenDecoded->jti,
            AdminLogoutMessageEnum::REFRESH
        );

        // 生成新的Tokens
        $systemAdmin = [
            'id' => $refreshTokenDecoded->userId,
            'nickname' => $refreshTokenDecoded->nickname,
            'avatar' => $refreshTokenDecoded->avatar,
        ];
        $tokens = $this->generateTokens($refreshTokenDecoded->adminRole, $systemAdmin, $deviceId);

        return success($tokens);
    }

    /**
     * 生成AccessToken和RefreshToken
     */
    private function generateTokens($adminRole, $systemAdmin, $deviceId)
    {
        $this->updateLogoutMessage();

        $currentTime = time();
        $currentIp = request()->ip();

        // 生成AccessToken
        $accessPayload = [
            'iss' => $currentIp, // 令牌的签发者（Issuer），通常是应用或服务的域名
            'sub' => 'access_token', // 令牌的主题（Subject），标识令牌的类型或用途
            'aud' => 'user', // 令牌的受众（Audience），即令牌的预期接收者
            'iat' => $currentTime, // 令牌的签发时间（Issued At），以 UNIX 时间戳表示
            'exp' => $currentTime + $this->accessTtl, // 令牌的过期时间（Expiration Time），以 UNIX 时间戳表示
            'jti' => uniqid('access_'), // 令牌的唯一标识符（JWT ID），通常是随机字符串
            'refresh_jti' => uniqid('refresh_'), // 关联的RefreshToken ID

            'adminRole' => $adminRole,
            'userId' => $systemAdmin['id'],
            'nickname' => $systemAdmin['nickname'],
            'avatar' => $systemAdmin['avatar'],
        ];
        $accessToken = setAdminJWT($accessPayload);

        // 生成RefreshToken
        $refreshPayload = [
            'iss' => $currentIp,
            'sub' => 'refresh_token',
            'aud' => 'user',
            'iat' => $currentTime,
            'exp' => $currentTime + $this->refreshTtl,
            'jti' => $accessPayload['refresh_jti'],

            'access_jti' => $accessPayload['jti'],
            'access_exp' => $accessPayload['exp'],

            'adminRole' => $adminRole,
            'userId' => $systemAdmin['id'],
            'nickname' => $systemAdmin['nickname'],
            'avatar' => $systemAdmin['avatar']
        ];
        $refreshToken = setAdminJWT($refreshPayload);

        // 同步数据库
        Db::transaction(function () use (
            $systemAdmin,
            $accessToken,
            $refreshToken,
            $deviceId,
            $accessPayload,
            $refreshPayload,
            $currentIp
        ) {
            app()->make(SystemAdminDao::class)->update($systemAdmin['id'], [
                'current_access_token' => $accessToken,
                'current_refresh_token' => $refreshToken,
                'current_device_id' => $deviceId,
                'current_ip' => $currentIp,
            ]);

            $this->allowMoreJwt($systemAdmin);

            app()->make(AdminTokenLogDao::class)->save([
                'user_id' => $systemAdmin['id'],
                'access_token' => $accessToken,
                'access_token_jti' => $accessPayload['jti'],
                'access_token_expire_time' => date('Y-m-d H:i:s', $accessPayload['exp']),
                'refresh_token' => $refreshToken,
                'refresh_token_jti' => $accessPayload['refresh_jti'],
                'refresh_token_used' => 0,
                'device_id' => $deviceId,
                'is_logout' => 0,
                'ip' => request()->ip(),
            ]);

            // 更新刷新token缓存信息
            $this->updateRefreshTokenRedis($systemAdmin['id'], $deviceId, $refreshPayload['jti'], $this->refreshTtl);
        });

        return [
            'access_token' => $accessToken,
            'refresh_token' => $refreshToken,
            'expires_in' => $this->accessTtl
        ];
    }

    /**
     * 是否只允许一个jwt生效
     */
    private function allowMoreJwt($systemAdmin)
    {
        if (env('allow_more_jwt', false)) return;

        $validAdminTokenLog = app()->make(AdminTokenLogDao::class)->select([
            ['user_id', '=', $systemAdmin['id']],
            ['is_logout', '=', 0],
        ]);
        foreach ($validAdminTokenLog as $singleAdminTokenLog) {
            $this->updateLogoutListRedis(
                $singleAdminTokenLog->access_token_jti,
                $singleAdminTokenLog->access_token_expire_time,
                $singleAdminTokenLog->refresh_token_jti,
                AdminLogoutMessageEnum::GENERATE
            );
        }
    }

    /**
     * 更新刷新token缓存信息
     */
    private function updateRefreshTokenRedis($userId, $deviceId, $key, $ttl, $used = false)
    {
        CacheService::set($this->refreshRedisPrefix . $key, [
            'userId' => $userId,
            'deviceId' => $deviceId,
            'used' => $used
        ], $ttl);

        // 已使用同步数据库
        if ($used) {
            app()->make(AdminTokenLogDao::class)->update([
                'refresh_token_jti' => $this->refreshRedisPrefix . $key
            ], ['refresh_token_used' => 1]);
        }
    }

    /**
     * 退出登录
     */
    public function logout($userId)
    {
        $user = app()->make(SystemAdminDao::class)->find($userId, ['current_access_token, current_refresh_token']);
        if (
            !$user ||
            empty($user->current_access_token) ||
            empty($user->current_refresh_token)
        ) return fail('该用户未登录');

        $accessPayload = getAdminJWT($user->current_access_token);

        Db::transaction(function () use ($user, $accessPayload) {

            // 清空用户表当前Token
            app()->make(SystemAdminDao::class)->update($accessPayload->userId, [
                'current_access_token' => null,
                'current_refresh_token' => null,
                'current_device_id' => null,
                'current_ip' => null,
            ]);

            // 更新已注销列表
            $this->updateLogoutListRedis(
                $accessPayload->jti, 
                $accessPayload->exp, 
                $accessPayload->refresh_jti, 
                AdminLogoutMessageEnum::LOGOUT
            );
        });


        return success();
    }

    /**
     * accessToken放入已注销列表
     */
    private function updateLogoutListRedis($accessJti, $exp, $refreshJti, $logoutMessage)
    {
        $expireTime = strtotime($exp);

        Db::transaction(function () use ($accessJti, $expireTime, $refreshJti, $logoutMessage) {
            app()->make(AdminTokenLogDao::class)->update([
                'access_token_jti' => $accessJti,
            ], [
                'is_logout' => 1,
                'logout_time' => now(),
                'logout_message' => $logoutMessage,
            ]);

            CacheService::set($this->logoutListRedisPrefix . $accessJti, true, $expireTime - time());

            CacheService::delete($this->refreshRedisPrefix . $refreshJti);
        });
    }

    /**
     * 获取设备指纹
     */
    private function getDeviceId()
    {
        $userAgent = request()->header('User-Agent');

        return md5($userAgent . request()->ip() . $this->jwtKey);
    }

    /**
     * 检查授权Token是否在注销列表
     */
    public function checkLogoutList($token)
    {
        // 验证授权Token
        $accessTokenDecoded = getAdminJWT($token);

        // 检查授权Token是否在注销列表中
        $logoutList = $this->logoutListRedisPrefix . $accessTokenDecoded->jti;
        if (CacheService::has($logoutList)) abort(ApiCodeEnum::UNAUTHORIZED, '授权Token已失效');

        // 没有再查一下数据库
        $where = [
            ['access_token_jti', '=', $accessTokenDecoded->jti],
            ['access_token_expire_time', '>', now()],
            ['is_logout', '=', 1],
        ];
        $adminTokenLog = app()->make(AdminTokenLogDao::class)->find($where);

        if (!$adminTokenLog) return $accessTokenDecoded;

        // 同步到已注销列表
        $this->updateLogoutListRedis(
            $adminTokenLog->access_token_jti, 
            $adminTokenLog->access_token_expire_time, 
            $adminTokenLog->refresh_token_jti,
            AdminLogoutMessageEnum::LOGOUT
        );

        abort(ApiCodeEnum::UNAUTHORIZED, '授权Token已失效');
    }

    /**
     * 更新过期AccessToken备注
     */
    public function updateLogoutMessage()
    {
        $where = [
            ['access_token_expire_time', '>', now()],
            ['is_logout', '=', 0],
        ];

        app()->make(AdminTokenLogDao::class)->update($where, [
            'logout_message' => 'access_token已过期',
            'is_logout' => 1
        ]);
    }
}